﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using Jurassic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Actors;
using System.Threading.Tasks;
using System.Threading.Actors.Languages;
using System.Diagnostics;
using System.Threading.Actors.Utilities;
using System.Reflection;

namespace TestActorMethods
{
    [TestClass]
    public class JavascriptUnitTests
    {

        // Test that javascript/C# interaction works.  (Or at least mostly works;
        // conveying ints between the two is a dicey proposition.)
        [TestMethod]
        public void JavascriptInterop_Test()
        {
            JavascriptInterop_Test_impl(ActorStateEnum.Standalone);
            JavascriptInterop_Test_impl(ActorStateEnum.InMemory);
        }

        private void JavascriptInterop_Test_impl(ActorStateEnum stateType)
        {
            var state = ActorUnitTests.CreateActorState(stateType);
            state.AddLanguagePack(SupportedLanguages.Javascript, () => new JavascriptLanguagePackage(state));

            // Let's construct a counter maintenance method, in C#
            state.Set("BumpCount", (Func<dynamic, dynamic[], object>)delegate(dynamic astate, dynamic[] args)
            {
                int amountToBump = args[0];
                int currentCount = ((object)astate.ValueOrDefault("_count", 0)).ToInt();
                int newCount = currentCount + amountToBump;
                astate._count = newCount;
                return newCount;
            });

            // Now let's add a couple of JS methods: clear, and set.
            var setMethodImpl = @"
                function setCount(state, value)
                {
                    state.set('_count', value);
                    return value;
                }";
            var clearMethodImpl = @"
                function clearCount(state)
                {
                    state.set('_count', 0);
                    return 0;
                }";
            state.AddMethod(SupportedLanguages.Javascript, "setCount", setMethodImpl);
            state.AddMethod(SupportedLanguages.Javascript, "clearCount", clearMethodImpl);
                
            // Now a CS method that calls into a JS method
            state.Set("ClearSpecial", (Func<dynamic, dynamic[], object>)delegate(dynamic astate, dynamic[] aparameters)
            {
                // Clears the count, but returns the value before being cleared
                int oldValue = astate.ValueOrDefault("_count", 0);
                astate.clearCount(); // call JS method
                return oldValue;
            });

            // Now a JS method that calls into a CS method
            var getMethodImpl = @"
                function getCount(state)
                {
                    //rval = state.BumpCount(0);
                    var rval = state.callMethod('BumpCount',0);
                    var oval = JSON.parse(rval);
                    return oval;
                }";
            state.AddMethod(SupportedLanguages.Javascript, "getCount", getMethodImpl);

            // Now a JS method that calls into another JS method
            var conditionalSetImpl = @"
                function condSet(state, value)
                {
                    var ret = state.callMethod('getCount');
                    var currcount = JSON.parse(ret);
                    if(currcount > value)
                    {
                        state.callMethod('setCount', value);
                    }
                    return state.get('_count');
                }";
            state.AddMethod(SupportedLanguages.Javascript, "condSet", conditionalSetImpl);

            // Let's start testing!
            int count = state.CallMethod("getCount", new object[0]).ToInt();
            Assert.IsTrue(count == 0, "Expected initial getCount to return 0");
            count = state.Get("_count").ToInt();
            Assert.IsTrue(count == 0, "Expected initial Get(_count) to return 0");
            count = state.CallMethod("setCount", new object[] { 23 }).ToInt();
            Assert.IsTrue(count == 23, "Expected setCount(23) to return 23");
            count = state.CallMethod("condSet", new object[] { 20 }).ToInt();
            Assert.IsTrue(count == 20, "Expected condSet(20) to return 20");
            count = state.Get("_count").ToInt();
            Assert.IsTrue(count == 20, "Expected Get(_count) after condset(20) to return 20");
            count = (int)state.CallMethod("BumpCount", new object[] { 15 });
            Assert.IsTrue(count == 35, "Expected initial BumpCount(15) to return 35");
            count = state.CallMethod("getCount", new object[0]).ToInt();
            Assert.IsTrue(count == 35, "Expected getCount after BumpCount(15) to return 35");
            count = (int)state.CallMethod("ClearSpecial", new object[0]);
            Assert.IsTrue(count == 35, "Expected ClearSpecial to return 35");
            count = state.CallMethod("getCount", new object[0]).ToInt();
            Assert.IsTrue(count == 0, "Expected getCount after ClearSpecial to return 0");
            count = state.Get("_count").ToInt();
            Assert.IsTrue(count == 0, "Expected Get(_count) after ClearSpecial to return 0");

        }

        [TestMethod]
        public void JavascriptTypes_Test()
        {
            JavascriptTypes_Test_impl(ActorStateEnum.Standalone);
            JavascriptTypes_Test_impl(ActorStateEnum.InMemory);
        }

        private void JavascriptTypes_Test_impl(ActorStateEnum stateType)
        {
            var state = ActorUnitTests.CreateActorState(stateType);
            state.AddLanguagePack(SupportedLanguages.Javascript, () => new JavascriptLanguagePackage(state));

            var setStringImpl = @"
                function setString(state, value)
                {
                    state.set('_str', value);
                    return value;
                }";
            var getStringImpl = @"
                function getString(state)
                {
                    var value = state.get('_str');
                    return value;
                }";
            var setNumberImpl = @"
                function setNumber(state, value)
                {
                    state.set('_num', value);
                    return value;
                }";
            var getNumberImpl = @"
                function getNumber(state)
                {
                    var value = state.get('_num');
                    return value;
                }";
            state.AddMethod(SupportedLanguages.Javascript, "setString", setStringImpl);
            state.AddMethod(SupportedLanguages.Javascript, "setNumber", setNumberImpl);
            state.AddMethod(SupportedLanguages.Javascript, "getString", getStringImpl);
            state.AddMethod(SupportedLanguages.Javascript, "getNumber", getNumberImpl);

            string val;
            val = (string)state.CallMethod("setString", new object[] { "abc" });
            Assert.IsTrue(val == "abc", "Expected setString('abc') to return abc");
            val = state.Get("_str").ToString();
            Assert.IsTrue(val == "abc", "Expected setString('abc') to return abc");
            val = (string)state.Get("_str");
            Assert.IsTrue(val == "abc", "Expected value at _str to be abc");
            val = (string)state.CallMethod("getString", new object[] { });
            Assert.IsTrue(val == "abc", "Expected getString() to return abc");

            double nval;
            object rc;
            rc = state.CallMethod("setNumber", new object[] { 5 });
            nval = Convert.ToDouble(rc);
            Assert.IsTrue(nval == 5d, "Expected setNumber(5) to return 5");
            nval = (double)state.Get("_num");
            Assert.IsTrue(nval == 5d, "Expected value at _num to be 5");
            nval = (double)state.CallMethod("getNumber", new object[] { });
            Assert.IsTrue(nval == 5d, "Expected getNumber() to return 5");
        }

        // Test actor-to-actor communications facilities in javascript.
        [TestMethod]
        public void JavascriptA2A_Test()
        {
            var state = new StandaloneActorState("me");
            state.AddLanguagePack(SupportedLanguages.Javascript, () => new JavascriptLanguagePackage(state));

            // Set up second actor/state
            var otherState = new StandaloneActorState("someOtherActor");
            otherState.Set("someMethod", (Func<IActorState, object[], object>)delegate(IActorState astate, object[] aparameters)
            {
                return "someMethod: " + aparameters.Select(o => o.ToString()).Aggregate("", (acc, item) => acc + " " + item);
            });

            // Check variadic call from javascript
            var calloutMethodImpl = @"
                    function callout(state, value)
                    {
                        proxy = state.getActorProxy('someOtherActor');
                        return proxy.request('someMethod', 1, 42.0, 'one').getResult();
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "callout", calloutMethodImpl);

            var s1 = (string)state.CallMethod("callout", new object[0]);
            Assert.IsTrue(s1 != null, "Expected non-null return from js \"callout\" method");

            Console.WriteLine("return from callout is {0}", s1);

            // Give ourselves a continuation handler
            var continuationImpl = @"
                    function continuationHandler(state, promise)
                    {
                        state.set('resultKey', promise.getResult());
                        return null;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "continuationHandler", continuationImpl);

            // Give ourselves an event handler
            var eventHandlerImpl = @"
                    function eventHandler(state,event)
                    {
                        //return eval('(' + event.getPayload() + ')'); // strip json quotes from returned string
                        return JSON.parse(event.getPayload()); // strip json quotes from returned string
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "eventHandler", eventHandlerImpl);

            // Check variadic call into javascript
            var variadicMethodImpl = @"
                    function variadic(state, value) // accepts variadic input, makes a variadic call
                    {
                        proxy = state.getActorProxy('someOtherActor');
                        var args = Array.prototype.slice.call(arguments,1); // drop actor state from arguments
                        args.unshift('someMethod'); // prepend method name to arguments
                        var promise = proxy.request.apply(proxy,args);
                        var rval = promise.getResult(); // make sure that we complete before lining up continuation
                        promise.continueWith('continuationHandler');
                        return rval;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "variadic", variadicMethodImpl);

            var callResult = state.CallMethod("variadic", new object[] { "foo", "bar", 42.0 });
            var s2 = (string)callResult;
            Assert.IsTrue(s2 != null, "Expected non-null return from js \"variadic\" method");
            Console.WriteLine("return from variadic is {0}", s2);
            object temp;
            // It may take a while for the continuation to complete after the promise completes
            Assert.IsTrue(state.WaitForKey("resultKey", 1000), "Expected \"resultKey\" to be set after call to variadic");
            temp = state.Get("resultKey");
            Console.WriteLine("resultKey = {0}", temp);

            // We need to make sure that event handlers properly ingest ActorEvents.  
            // It's not really easy to simulate an actual event publication, so I'll just call the handler directly.
            ActorEvent ae = new ActorEvent("myEventType", "myEventPayload");
            var eventHandlerResult = (string) state.CallMethod("eventHandler", new object[] { ae });
            Assert.IsTrue(eventHandlerResult.Equals("myEventPayload"), "Expected eventHandler to return \"myEventPayload\"");

            // Test the ability to create and delete actors from javascript actor method.
            var actorCreatorImpl = @"
                    function actorCreator(state, nameOfActor) // creates an actor and gets a proxy to it
                    {
                        state.createActor(nameOfActor, 'nothing');
                        proxy = state.getActorProxy(nameOfActor);
                        return null;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "actorCreator", actorCreatorImpl);
            var actorDestroyerImpl = @"
                    function actorDestroyer(state, nameOfActor) // creates and destroys an actor
                    {
                        state.deleteActor(nameOfActor);
                        return null;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "actorDestroyer", actorDestroyerImpl);

            state.CallMethod("actorCreator", new object[] { "createdActor" });
            StandaloneActorState lookedUpState = null;
            Assert.IsTrue(StandaloneActorState.TryLookupActorState("createdActor", null, out lookedUpState));
            state.CallMethod("actorDestroyer", new object[] { "createdActor" });
            Assert.IsFalse(StandaloneActorState.TryLookupActorState("createdActor", null, out lookedUpState));
        }

        [TestMethod]
        public void JavascriptA2AContinuationTest()
        {
            var state = new StandaloneActorState("me");
            state.AddLanguagePack(SupportedLanguages.Javascript, () => new JavascriptLanguagePackage(state));

            // Set up second actor/state
            var otherState = new StandaloneActorState("someOtherActor");
            otherState.AddLanguagePack(SupportedLanguages.Javascript, () => new JavascriptLanguagePackage(otherState));

            var continuationImpl = @"
                    function continuationHandler(state, promise)
                    {
                        // Consider making this take a long time, to trigger any potential race by not waiting 
                        // on ContinueWith's returned promise.
                        state.set('result', promise.getResult());
                        return null;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "continuationHandler", continuationImpl);

            var methodImpl = @"
                    function method1(state, parameters)
                    {                        
                        return 42;
                    }";
            otherState.AddMethod(SupportedLanguages.Javascript, "method1", methodImpl);

            // Now from the first actor, get a proxy to the other actor and call method1 on it.
            // Call ContinueWith on an IActorPromise, then get the second IActorPromise returned by
            // ContinueWith.  Later, block on that and read the state written by the continuation.
            var testMethodImpl = @"
                    function CallAndRunContinuation(state, value) // accepts variadic input, makes a variadic call
                    {
                        proxy = state.getActorProxy('someOtherActor');
                        var args = Array.prototype.slice.call(arguments,1); // drop actor state from arguments
                        args.unshift('method1'); // prepend method name to arguments
                        var promise = proxy.request.apply(proxy,args);
                        var rval = promise.getResult(); // make sure that we complete before lining up continuation
                        var continuationPromise = promise.continueWith('continuationHandler');
                        // Do not block on the continuation here - we'll deadlock.
                        return continuationPromise;
                    }";
            state.AddMethod(SupportedLanguages.Javascript, "CallAndRunContinuation", testMethodImpl);

            Object continuationPromise = state.CallMethod("CallAndRunContinuation", new Object[0]);
            // The return value is a PromiseInstance, an internal type.  Let's call its Wait method.
            MethodInfo waitMethod = continuationPromise.GetType().GetMethod("Wait", BindingFlags.Public | BindingFlags.Instance);
            waitMethod.Invoke(continuationPromise, null);
            // Now we can read 'result' from our IActorState safely.

            Object storedResult = null;
            if (state.TryGet("result", out storedResult))
            {
                Assert.IsTrue((String)storedResult == "42", "Expected \"result\" to be set to \"42\" after JavaScript method call w/ continuation and waited on the promise.  Stored result was: " + storedResult);
            }
            else
                Assert.Fail("After calling a method and blocking on an appropriate continuation, the result wasn't set.");
        }
    }
}
